package felix.rule.js.script;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import felix.rule.data.msg.EngineMsg;
import felix.rule.data.msg.EngineMsgMetaData;
import lombok.extern.slf4j.Slf4j;

import javax.script.ScriptException;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

@Slf4j
public class ScriptEngineImpl implements ScriptEngine {

    private static final ObjectMapper mapper = new ObjectMapper();
    private final JsInvokeService sandboxService;

    private final UUID scriptId;

    public ScriptEngineImpl(JsInvokeService sandboxService, String script) {
        this.sandboxService = sandboxService;
        try {
            this.scriptId = this.sandboxService.eval(script).get();
        } catch (Exception e) {
            Throwable t = e;
            if (e instanceof ExecutionException) {
                t = e.getCause();
            }
            throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t);
        }
    }

    private static String[] prepareArgs(EngineMsg msg) {
        try {
            String[] args = new String[4];
            args[0] = msg.getId().toString();
            if (msg.getData() != null) {
                args[1] = mapper.writeValueAsString(msg.getData());
            } else {
                args[1] = "";
            }
            args[2] = mapper.writeValueAsString(Optional.ofNullable(msg.getMetaData()).orElse(new EngineMsgMetaData()).getData());
            args[3] = msg.getType();
            return args;
        } catch (Throwable th) {
            throw new IllegalArgumentException("Cannot bind js args", th);
        }
    }

    @Override
    public ListenableFuture<String> executeToStringAsync(EngineMsg msg) {
        ListenableFuture<JsonNode> result = executeScriptAsync(msg);
        return Futures.transformAsync(result, json -> {
            if (!json.isTextual()) {
                log.warn("Wrong result type: {}", json.getNodeType());
                return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
            } else {
                return Futures.immediateFuture(json.asText());
            }
        }, sandboxService.getJsExecutor());
    }

    @Override
    public JsonNode executeToJson(EngineMsg msg) throws ScriptException {
        return executeScript(msg);
    }

    @Override
    public ListenableFuture<JsonNode> executeToJsonAsync(EngineMsg msg) {
        return executeScriptAsync(msg);
    }

    @Override
    public String executeToString(EngineMsg msg) throws ScriptException {
        JsonNode result = executeScript(msg);
        if (!result.isTextual()) {
            log.warn("Wrong result type: {}", result.getNodeType());
            throw new ScriptException("Wrong result type: " + result.getNodeType());
        }
        return result.asText();
    }

    @Override
    public boolean executeToBoolean(EngineMsg msg) throws ScriptException {
        JsonNode result = executeScript(msg);
        if (!result.isBoolean()) {
            log.warn("Wrong result type: {}", result.getNodeType());
            throw new ScriptException("Wrong result type: " + result.getNodeType());
        }
        return result.asBoolean();
    }

    @Override
    public ListenableFuture<Boolean> executeToBooleanAsync(EngineMsg msg) {
        ListenableFuture<JsonNode> result = executeScriptAsync(msg);
        return Futures.transformAsync(result, json -> {
            if (!json.isBoolean()) {
                log.warn("Wrong result type: {}", json.getNodeType());
                return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
            } else {
                return Futures.immediateFuture(json.asBoolean());
            }
        }, sandboxService.getJsExecutor());
    }

    @Override
    public ListenableFuture<Set<String>> executeToSetAsync(EngineMsg msg) {
        ListenableFuture<JsonNode> result = executeScriptAsync(msg);
        return Futures.transformAsync(result, json -> {
            if (json.isTextual()) {
                return Futures.immediateFuture(Collections.singleton(json.asText()));
            } else if (json.isArray()) {
                Set<String> nextStates = Sets.newHashSet();
                for (JsonNode val : json) {
                    if (!val.isTextual()) {
                        log.warn("Wrong result type: {}", val.getNodeType());
                        return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
                    } else {
                        nextStates.add(val.asText());
                    }
                }
                return Futures.immediateFuture(nextStates);
            } else {
                log.warn("Wrong result type: {}", json.getNodeType());
                return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType()));
            }
        }, sandboxService.getJsExecutor());
    }

    @Override
    public Set<String> executeToSet(EngineMsg msg) throws ScriptException {
        JsonNode result = executeScript(msg);
        if (result.isTextual()) {
            return Collections.singleton(result.asText());
        } else if (result.isArray()) {
            Set<String> nextStates = Sets.newHashSet();
            for (JsonNode val : result) {
                if (!val.isTextual()) {
                    log.warn("Wrong result type: {}", val.getNodeType());
                    throw new ScriptException("Wrong result type: " + val.getNodeType());
                } else {
                    nextStates.add(val.asText());
                }
            }
            return nextStates;
        } else {
            log.warn("Wrong result type: {}", result.getNodeType());
            throw new ScriptException("Wrong result type: " + result.getNodeType());
        }
    }

    private JsonNode executeScript(EngineMsg msg) throws ScriptException {
        try {
            String[] inArgs = prepareArgs(msg);
            String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2], inArgs[3])
                    .get().toString();
            return mapper.readTree(eval);
        } catch (ExecutionException e) {
            if (e.getCause() instanceof ScriptException) {
                throw (ScriptException) e.getCause();
            } else if (e.getCause() instanceof RuntimeException) {
                throw new ScriptException(e.getCause().getMessage());
            } else {
                throw new ScriptException(e);
            }
        } catch (Exception e) {
            throw new ScriptException(e);
        }
    }

    private ListenableFuture<JsonNode> executeScriptAsync(EngineMsg msg) {
        String[] inArgs = prepareArgs(msg);
        return Futures.transformAsync(sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]),
                o -> {
                    try {
                        return Futures.immediateFuture(mapper.readTree(o.toString()));
                    } catch (Exception e) {
                        if (e.getCause() instanceof ScriptException) {
                            return Futures.immediateFailedFuture(e.getCause());
                        } else if (e.getCause() instanceof RuntimeException) {
                            return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
                        } else {
                            return Futures.immediateFailedFuture(new ScriptException(e));
                        }
                    }
                }, sandboxService.getJsExecutor());
    }

    @Override
    public void destroy() {
        sandboxService.release(this.scriptId);
    }
}
